本文首发于先知社区:https://xz.aliyun.com/t/11087
去年weblogic出白名单时研究了下怎么绕过,总结出了下面的思路,本想再找找有无新的攻击面的思路,但是找了几次都没找到,后来就搁置了。昨天看见https://xz.aliyun.com/t/11037这篇文章,提到了T3协议绕过,才想起自己也搞过这块的研究,遂将研究的内容分享出来抛砖引玉,希望能看到大佬们更多的分析文章。
0x01 协商
Weblogic处理T3基础信息协商的类如下
weblogic.rjvm.t3.MuxableSocketT3#readIncomingConnectionBootstrapMessage
客户端发送:
服务端发送:
1 2 3 4 5
| HELO:12.2.1.4.false AS:2048 HL:19 MS:10000000 PN:DOMAIN
|
客户端发送的都是这种键值对的形式,存在以下可用键:
其中,AS和HL是两种常用的头信息,这里和我们利用的关系不大,就不分析了,需要注意的时候AS需要设置成01,尽可能小。
0x02 信息发送
信息处理的主要代码在weblogic.rjvm.MsgAbbrevInputStream#init函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| super.init(data, 4);#变量初始化,以及跳过一个int(数据总长度) this.connection = connection; this.responseId = -1; this.user = null; this.setValidatingClass(false); this.header.readHeader(this, connection.getRemoteHeaderLength());#读取header信息 if (this.connectionManager.thisRJVM != null) { this.header.src = this.connectionManager.thisRJVM.getID(); }
this.header.dest = JVMID.localID(); if (this.requiresUnauthenticatedFilter()) { WebLogicObjectInputFilter.setUnauthenticatedFilterForStream(this.objectStream); this.objectStream.setFilterType(MsgAbbrevInputStream.FilterType.UNAUTHENTICATED); } else if (this.objectStream.getFilterType() == null) { WebLogicObjectInputFilter.setWebLogicFilterForStream(this.objectStream); this.objectStream.setFilterType(MsgAbbrevInputStream.FilterType.WLS); }
if (KernelStatus.DEBUG && debugMessaging.isDebugEnabled()) { }
this.mark(this.header.abbrevOffset); this.skip((long)(this.header.abbrevOffset - this.pos())); connection.readMsgAbbrevs(this); this.reset(); if (JVMID.localID().equals(this.header.dest)) { if (!this.header.getFlag(8)) { this.read81Contexts(); } else { this.readExtendedContexts(); } }
|
第一步super.init()会进行一些初始化,并跳过前面的4个byte的长度信息,首先读取的就是header信息,处理函数如下weblogic.rjvm.JVMMessage#readHeader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| try { this.cmd = JVMMessage.Command.getByValue(is.readByte()); this.QOS = is.readByte(); this.flags = is.readByte() & 255; this.hasJVMIDs = this.getFlag(1); this.hasTX = this.getFlag(2); this.hasTrace = this.getFlag(4); this.responseId = is.readInt(); this.invokableId = is.readInt(); this.abbrevOffset = is.readInt(); int skip = remoteHeaderLen - 19; if (skip > 0) { is.skip((long)skip); }
} catch (IOException var4) { throw new AssertionError("Exception reading message header", var4); }
|
总的一共是19个byte
这里说一些比较重要的
- cmd,代表的是执行指令,这个值会影响代码进入不同的处理分支
- flags,一个标志位,标识数据包中的信息种类,同样会影响代码进入不同分支
- abbreOffset,一个int变量,标识header长度,在后面对流的控制会用到。
在读取完header后,会调用mark和skip函数,标记当前位置,并跳过一部分内容,跳过的长度为abbrevOffset 的值-当前读取的长度,然后调用connection.readMsgAbbrevs(this);读取信息。
1 2
| this.skip((long)(this.header.abbrevOffset - this.pos()));
|
readMsgAbbrevs函数就会对流中的序列化数据进行反序列化,调用的是InboundMsgAbbrev类的readObject方法,并存储在栈中。调用栈如下
会调用到FilteringObjectInputStream的resolveClass函数。这里也就是之前weblogic的漏洞会触发的readObject的地方。但是在21年4月的补丁中,Weblogic使用了白名单,只有以下七种类可以被反序列化,因此所有Weblogic原本的漏洞都无法使用。
- java.lang.String
- weblogic.rmi.spi.ServiceContext
- weblogic.rjvm.ClassTableEntry
- weblogic.rjvm.JVMID
- weblogic.security.acl.internal.AuthenticatedUser
- weblogic.rmi.extensions.server.RuntimeMethodDescriptor
- weblogic.utils.io.Immutable
读取结束后,执行reset()方法,流的指针回到之前mark处,根据header中flag值的不同,进入不同的分支。
1 2 3 4 5
| if (!this.header.getFlag(8)) { this.read81Contexts(); } else { this.readExtendedContexts(); }
|
根据需求设定flag值,可以进入如下函数
该函数的关键代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| if (b == 4) { ObjectStreamClass desc = this.readClassDescriptor(); Class cl = this.resolveClass(desc); weblogic.utils.io.ObjectStreamClass osc = weblogic.utils.io.ObjectStreamClass.lookup(cl); Externalizable e = (Externalizable)osc.newInstance(); int envelopeLength = this.readInt(); int startEnvelope = this.pos(); this.pushExternalizableInfo(startEnvelope + envelopeLength, cl.getName()); boolean var12 = false; try { var12 = true; e.readExternal(this); var12 = false; }
|
这部分代码就是T3白名单绕过的关键部分了。
白名单绕过
上面的函数中实例化了一个实现了Externalizable接口的类,并调用了它的readExternal。这个类的Desc信息来源于this.readClassDescriptor();
这个函数会从栈中的ClassTableEntry中读取descriptor属性作为desc,接着调用resovleClass方法生成类,这里调用的是MsgAbbrevInputStream的resolveClass方法,只存在黑名单判断,不存在白名单。接着会实例化这个类,并调用readExternal方法。栈中的ClassTableEntry类是在Weblogic T3的白名单中的,因此可以顺利被传入。
在后面在readExternal中需要注意,要想真正绕过白名单,不能在Externalizable 实例的readExternal里调用原生的readObject方法,不然还是会受黑名单影响。
举例,在传入的ClassTableEntry对象的descriptor属性中传入weblogic.cache.RefWrapper对应的ObjectStreamClass对象。该类实现了Externalizable接口,同时该类的readExternal方法如下:
1 2 3 4 5 6 7 8 9 10 11
| public void readExternal(ObjectInput oi) throws IOException, ClassNotFoundException { Object o = oi.readObject(); if (o != null) { if (nosoftrefs) { this.hardref = o; } else { this.softref = new SoftReference(o); }
} }
|
在这个in.readObject()
处打个断点,,按照文章前面提到的数据发送流程发送数据后,再进入readObject跟几步,利用栈如下:
可以发现程序又进入了readObjectFromPreDiabloPeer方法。原因在于默认传入的ObjectInput和在MsgAbbrevInputStream中被readObject的是同一个流,依然会受白名单的影响。那么如何把这个流替换成其他的呢?其实也很简单。 
在Externalizable接口的实现类中,很常见的会看见这种写法,这里以com.tangosol.coherence.servlet.AttributeHolder为例
1 2 3 4 5 6 7
| public void readExternal(DataInput in) throws IOException { this.m_sName = ExternalizableHelper.readUTF(in); this.m_oValue = ExternalizableHelper.readObject(in); this.m_fActivationListener = in.readBoolean(); this.m_fBindingListener = in.readBoolean(); this.m_fLocal = in.readBoolean(); }
|
它在反序列化的过程中使用的是ExternalizableHelper.readObject方法。它在反序列化过程中根据序列化的数据类型不同,存在许多自定义的逻辑。其中在反序列化利用链中最常用是下面这两种,我们重点关注它们对流是否存在转换和处理
第一个是反序列化实现了ExternalizableLite接口的类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| try { Class<?> clz = loadClass(sClass, loader, inWrapper == null ? null : inWrapper.getClassLoader()); if (in instanceof ObjectInputStream) { ObjectInputStream ois = (ObjectInputStream)in; if (!checkObjectInputFilter(clz, ois)) { throw new InvalidClassException("Deserialization of class " + sClass + " was rejected"); } }
value = (ExternalizableLite)clz.newInstance(); } catch (InstantiationException var7) { throw new IOException("Unable to instantiate an instance of class '" + sClass + "'; this is most likely due to a missing public no-args constructor: " + var7 + "\n" + getStackTrace(var7) + "\nClass: " + sClass + "\nClassLoader: " + loader + "\nContextClassLoader: " + getContextClassLoader()); } catch (Exception var8) { throw new IOException("Class initialization failed: " + var8 + "\n" + getStackTrace(var8) + "\nClass: " + sClass + "\nClassLoader: " + loader + "\nContextClassLoader: " + getContextClassLoader(), var8); }
if (loader != null) { if (inWrapper == null) { in = new WrapperDataInputStream((DataInput)in, loader); } else if (loader != inWrapper.getClassLoader()) { inWrapper.setClassLoader(loader); } }
value.readExternal((DataInput)in); if (value instanceof SerializerAware) { ((SerializerAware)value).setContextSerializer(ensureSerializer(loader)); }
|
首先是一段黑名单判断,这应该是之前某个二次反序列化话的补丁。黑名单后就会newInstance,然后会生成一个新的WrapperDataInputStream对象,这看起来像是对流进行了转化,但其实只是一层封装,在实际的readObject过程中还是使用的原始流。
1 2 3 4 5 6 7
| if (loader != null) { if (inWrapper == null) { in = new WrapperDataInputStream((DataInput)in, loader); } else if (loader != inWrapper.getClassLoader()) { inWrapper.setClassLoader(loader); } }
|
因此重点需要关注第二个readSerializable方法了。这个方法是对常规的序列化的封装。在执行readObject前存在这样一段代码:
1 2
| ObjectInput streamObj = getObjectInput(in, loader);
|
在高版本的Weblogic中,最终是执行下面这段代码:
1 2 3 4 5 6 7 8 9
| public ObjectInput getObjectInput(DataInput in, ClassLoader loader, boolean fForceNew) throws IOException { if (!fForceNew && in instanceof WLSObjectInputStream) { return (ObjectInput)in; } else { InputStream inStream = this.getInputStream(in, fForceNew); loader = loader == null && in instanceof WrapperDataInputStream ? ((WrapperDataInputStream)in).getClassLoader() : loader; return (ObjectInput)(loader == null && in instanceof FilteringObjectInputStream ? (ObjectInput)in : new WLSObjectInputStream(inStream, RemoteObjectReplacer.getReplacer(), new ClassLoaderResolver(loader), loader, this.setFilter)); } }
|
在T3反序列化中,会生成一个新的WLSObjectInputStream对象作为流,从而摆脱了白名单。
下面是测试的调用栈。
在进入WLSObjectInputStream的readObject后,构建符合要求的数据流,即可正常反序列化,在这一步的实操中,我利用程序自身的序列化方法进行序列化的数据,在反序列化时都失败了,可能需要对字节码的一些字段进行手动修改,本文只提出T3协议的绕过方法,后续的WLSObjectInputStream的readObject实现,有兴趣的师傅可以自行尝试。